<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>飞行小游戏</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
            height: 100%;
        }
        #world {
            position: absolute;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: linear-gradient(#e4e0ba, #f7d9aa);
        }
    </style>
</head>
<body>
    <div id="world"></div>
    <script type="text/javascript" src="../lib/three.js"></script>
    <script>
        var Colors = {
            red:0xf25346,
            white:0xd8d0d1,
            brown:0x59332e,
            pink:0xF5986E,
            brownDark:0x23190f,
            blue:0x68c3c0,
        };

        var scene, camera, fieldOfView, aspectRatio, nearPlane, farPlane, HEIGHT, WIDTH, renderer, container;

        window.addEventListener('load', init, false);

        function init() {
            createScene();
            createLights();
            createPlane();
            createSea();
            createSky();
            // var axesHelper = new THREE.AxesHelper( 100 );
            // scene.add( axesHelper );

            // 添加监听器
            document.addEventListener('mousemove', handleMouseMove, false);

            loop();
        }

        function createScene() {
            // 获得屏幕的高宽，用于设置相机的纵横比以及渲染器的大小
            HEIGHT = window.innerHeight;
            WIDTH = window.innerWidth;

            scene = new THREE.Scene();
            // 在场景中添加雾的效果；样式上使用和背景一样的颜色
            // scene.fog = new THREE.Fog(0xf7d9aa, 100, 950);

            // 创建相机
            aspectRatio = WIDTH / HEIGHT;
            fieldOfView = 60;
            nearPlane = 1;
            farPlane = 10000;
            camera = new THREE.PerspectiveCamera(fieldOfView, aspectRatio, nearPlane, farPlane);
            // 设置相机位置
            camera.position.x = 0;
            camera.position.y = 100;
            camera.position.z = 200;

            // 创建渲染器
            renderer = new THREE.WebGLRenderer({
                // 在 css 中设置背景色透明显示渐变色
                alpha: true,
                // 开启抗锯齿
                antialias: true
            });
            renderer.setSize(WIDTH, HEIGHT);
            // 打开渲染器的阴影地图
            renderer.shadowMap.enabled = true;
            
            // 在 HTML 创建的容器中添加渲染器的 DOM 元素
            container = document.getElementById('world');
            container.appendChild(renderer.domElement);

            // 监听屏幕，缩放屏幕更新相机和渲染器的尺寸
            window.addEventListener('resize', handleWindowResize, false);
        }

        function handleWindowResize() {
            HEIGHT = window.innerHeight;
            WIDTH = window.innerWidth;
            renderer.setSize(WIDTH, HEIGHT);
            camera.aspect = WIDTH / HEIGHT;
            camera.updateProjectionMatrix();
        }

        var hemisphereLight, shadowLight, ambientLight;
        function createLights() {
            // 半球光就是渐变的光
            // 第一个参数是天空的颜色，第二个参数是地上的颜色，第三个参数是光源的强度
            hemisphereLight = new THREE.HemisphereLight(0xaaaaaa, 0x000000, .6);
            // 方向光
            // 第一个参数是光线颜色，第二个参数是光源强度
            shadowLight = new THREE.DirectionalLight(0xffffff, .8);
            // 环境光源修改场景中的全局颜色和使阴影更加柔和
            ambientLight = new THREE.AmbientLight(0xdc8874, .5);
            // 设置光源的方向
            shadowLight.position.set(150, 350, 350);
            // 开启光源投影
            shadowLight.castShadow = true;
            
            // 定义可见域的投射阴影
            shadowLight.shadow.camera.left = -400;
            shadowLight.shadow.camera.right = 400;
            shadowLight.shadow.camera.top = 400;
            shadowLight.shadow.camera.bottom = -400;
            shadowLight.shadow.camera.near = 1;
            shadowLight.shadow.camera.far = 1000;

            // 定义阴影的分辨率
            shadowLight.shadow.mapSize.width = 2048;
            shadowLight.shadow.mapSize.height = 2048;

            scene.add(hemisphereLight);
            scene.add(shadowLight);
            scene.add(ambientLight);
        }

        function Sea() {
            // 创建一个圆柱体（顶面半径，底面半径，高度，半径分段，高度分段
            var geom = new THREE.CylinderGeometry(600, 600, 800, 40, 10);
            // 在 x 轴旋转几何体
            geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
            // 重点：通过合并顶点，我们确保海浪的连续性
            geom.mergeVertices();
            var l = geom.vertices.length;
            // create an array to store new data associated to each vertex
            this.waves = [];
            for (var i=0; i<l; i++){
                // get each vertex
                var v = geom.vertices[i];
                // store some data associated to it
                this.waves.push({
                    y: v.y,
                    x: v.x,
                    z: v.z,
                    // a random angle
                    ang: Math.random()*Math.PI*2,
                    // a random distance
                    amp: 5 + Math.random()*15,
                    // a random speed between 0.016 and 0.048 radians / frame
                    speed: 0.016 + Math.random()*0.032
                });
            };         
            // 创建材质
            var mat = new THREE.MeshPhongMaterial({
                color: Colors.blue,
                transparent: true,
                opacity: .8,
                shading: THREE.FlatShading
            });
            this.mesh = new THREE.Mesh(geom, mat);
            // 允许大海对象接收阴影
            this.mesh.receiveShadow = true;
        }
        Sea.prototype.moveWaves = function (){
            // get the vertices
            var verts = this.mesh.geometry.vertices;
            var l = verts.length;  
            for (var i=0; i<l; i++){
                var v = verts[i];               
                // get the data associated to it
                var vprops = this.waves[i];                
                // update the position of the vertex
                v.x = vprops.x + Math.cos(vprops.ang)*vprops.amp;
                v.y = vprops.y + Math.sin(vprops.ang)*vprops.amp;
                // increment the angle for the next frame
                vprops.ang += vprops.speed;
            }
            // Tell the renderer that the geometry of the sea has changed.
            // In fact, in order to maintain the best level of performance, 
            // three.js caches the geometries and ignores any changes
            // unless we add this line
            this.mesh.geometry.verticesNeedUpdate=true;
            sea.mesh.rotation.z += .005;
        }

        // 实例化大海对象，并添加至场景
        var sea;
        function createSea() {
            sea = new Sea();
            // 在场景底部，稍微推挤一下
            sea.mesh.position.y = -600;
            // 添加大海的网格至场景
            scene.add(sea.mesh);
        }

        function Cloud() {
            // 创建一个空的容器放置不同形状的云
            this.mesh = new THREE.Object3D();
            // 创建一个正方体，整个形状会被复制创建云
            var geom = new THREE.BoxGeometry(20, 20, 20);
            // 创建材质
            var mat = new THREE.MeshPhongMaterial({
                color: Colors.white
            });
            // 随机多次复制几何体
            var nBlocs = 3+Math.floor(Math.random()*3);
            for (var i=0; i<nBlocs; i++) {
                var m = new THREE.Mesh(geom, mat);
                // 随机设置位置与角度
                m.position.x = i*15;
                m.position.y = Math.random()*10;
                m.position.z = Math.random()*10;
                m.rotation.z = Math.random()*Math.PI*2;
                m.rotation.y = Math.random()*Math.PI*2;
                // 随机设置方块大小
                var s = .1 + Math.random() * .9;
                m.scale.set(s, s, s);
                // 允许方块产生和接收阴影
                m.castShadow = true;
                m.receiveShadow = true;
                // 将方块添加到一开始创建的容器中
                this.mesh.add(m);
            }
        }

        function Sky() {
            // 创建一个空的容器
            this.mesh = new THREE.Object3D();
            // 选取若干朵云散布在天空中
            this.nClouds = 20;
            // 把云均匀地散布
            // 我们需要根据统一的角度放置它们
            var stepAngle = Math.PI*2 / this.nClouds;
            // 创建云
            for (var i=0; i<this.nClouds; i++) {
                var c = new Cloud();
                // 为每朵云设置位置及角度，为此需要用到一点三角函数
                var a = stepAngle * i; // 云朵最终的角度
                var h = 750 + Math.random() * 200; // 坐标系中心点与云朵间的距离
                // 将极坐标系转换为笛卡尔坐标系
                c.mesh.position.y = Math.sin(a) * h;
                c.mesh.position.x = Math.cos(a) * h;
                // 根据云朵的位置将其进行旋转
                c.mesh.rotation.z = a + Math.PI / 2;
                // 为了更好的展示效果，我们将云朵放置在场景中的随机深度
                c.mesh.position.z = -400 - Math.random() * 400;
                // 为每朵云设置随机大小
                var s = 1 + Math.random() * 2;
                c.mesh.scale.set(s, s, s);
                // 将云朵添加到天空容器中
                this.mesh.add(c.mesh);
            }
        }

        // 初始化天空
        var sky;
        function createSky() {
            sky = new Sky();
            sky.mesh.position.y = -600;
            scene.add(sky.mesh);
        }

        function AirPlane() {
            this.mesh = new THREE.Object3D();

            // 创建机舱
            var geomCockpit = new THREE.BoxGeometry(80, 50, 50, 1, 1, 1);
            var matCockpit = new THREE.MeshPhongMaterial({
                color: Colors.red,
                shading: THREE.FlatShading
            });
            // 我们可以通过访问形状中顶点数组中一组特定的顶点
            // 然后移动它的 x, y, z 属性，目的是调整机舱的形状
            geomCockpit.vertices[4].y-=10;
            geomCockpit.vertices[4].z+=20;
            geomCockpit.vertices[5].y-=10;
            geomCockpit.vertices[5].z-=20;
            geomCockpit.vertices[6].y+=30;
            geomCockpit.vertices[6].z+=20;
            geomCockpit.vertices[7].y+=30;
            geomCockpit.vertices[7].z-=20;
            var cockpit = new THREE.Mesh(geomCockpit, matCockpit);
            cockpit.castShadow = true;
            cockpit.receiveShadow = true;
            this.mesh.add(cockpit);

            // 创建引擎
            var geomEngine = new THREE.BoxGeometry(20, 50, 50, 1, 1, 1);
            var matEngine = new THREE.MeshPhongMaterial({
                    color: Colors.white,
                    shading: THREE.FlatShading
            });
            var engine = new THREE.Mesh(geomEngine, matEngine);
            engine.position.x = 40;
            engine.castShadow = true;
            engine.receiveShadow = true;
            this.mesh.add(engine);

            // 创建机尾
            var geomTailPlane = new THREE.BoxGeometry(15, 20, 5, 1, 1, 1);
            var matTailPlane = new THREE.MeshPhongMaterial({
                color: Colors.red,
                shading: THREE.FlatShading
            });
            var tailPlane = new THREE.Mesh(geomTailPlane, matTailPlane);
            tailPlane.position.set(-35, 25, 0);
            tailPlane.castShadow = true;
            tailPlane.receiveShadow = true;
            this.mesh.add(tailPlane);

                // 创建机翼
            var geomSideWing = new THREE.BoxGeometry(40, 8, 150, 1, 1, 1);
            var matSideWing = new THREE.MeshPhongMaterial({
                color: Colors.red,
                shading: THREE.FlatShading
            });
            var sideWing = new THREE.Mesh(geomSideWing, matSideWing);
            sideWing.castShadow = true;
            sideWing.receiveShadow = true;
            this.mesh.add(sideWing);

            // 创建螺旋桨
            var geomPropeller = new THREE.BoxGeometry(20, 10, 10, 1, 1, 1);
            var matPropeller = new THREE.MeshPhongMaterial({
                color: Colors.brown,
                shading: THREE.FlatShading
            });
            this.propeller = new THREE.Mesh(geomPropeller, matPropeller);
            this.propeller.castShadow = true;
            this.propeller.receiveShadow = true;

            // 创建螺旋桨的桨叶
            var geomBlade = new THREE.BoxGeometry(1, 100, 20, 1, 1, 1);
            var matBlade = new THREE.MeshPhongMaterial({
                color: Colors.brownDark,
                shading: THREE.FlatShading
            });

            var blade = new THREE.Mesh(geomBlade, matBlade);
            blade.position.set(8, 0, 0);
            blade.castShadow = true;
            blade.receiveShadow = true;
            this.propeller.add(blade);
            this.propeller.position.set(50, 0, 0);
            this.mesh.add(this.propeller);
        }

        var airplane;
        function createPlane() {
            airplane = new AirPlane();
            airplane.mesh.scale.set(.25, .25, .25);
            airplane.mesh.position.y = 100;
            scene.add(airplane.mesh);
        }

        function loop() {
            // 使螺旋桨旋转并转动大海和云
            airplane.propeller.rotation.x += 0.3;
            sea.mesh.rotation.z += .005;
            sky.mesh.rotation.z += .01;
            // 更新飞机位置
            updatePlane();
            // 移动海浪
            sea.moveWaves();
            // 渲染场景
            renderer.render(scene, camera);
            // 重新调用 render() 函数
            requestAnimationFrame(loop);
        }

        function updatePlane() {
            var targetY = normalize(mousePos.y,-.75,.75,25, 175);
            var targetX = normalize(mousePos.x,-.75,.75,-100, 100);
            // 在每帧通过添加剩余距离的一小部分的值移动飞机
            airplane.mesh.position.y += (targetY-airplane.mesh.position.y)*0.1;
            // 剩余的距离按比例转动飞机
            airplane.mesh.rotation.z = (targetY-airplane.mesh.position.y)*0.0128;
            airplane.mesh.rotation.x = (airplane.mesh.position.y-targetY)*0.0064;
            airplane.propeller.rotation.x += 0.3;
        }

        function normalize(v,vmin,vmax,tmin, tmax){
            var nv = Math.max(Math.min(v,vmax), vmin);
            var dv = vmax-vmin;
            var pc = (nv-vmin)/dv;
            var dt = tmax-tmin;
            var tv = tmin + (pc*dt);
            return tv;
        }

        var mousePos = { x: 0, y: 0 };
        function handleMouseMove(event) {
            // 这里我把接收到的鼠标位置的值转换成归一化值，在-1与1之间变化 
            // 这是x轴的公式:
            var tx = -1 + (event.clientX / WIDTH)*2;
            // 对于 y 轴，我们需要一个逆公式
            // 因为 2D 的 y 轴与 3D 的 y 轴方向相反
            var ty = 1 - (event.clientY / HEIGHT)*2;
            mousePos = {x:tx, y:ty};
        }

        var Pilot = function(){
            this.mesh = new THREE.Object3D();
            this.mesh.name = "pilot";
            
            // angleHairs is a property used to animate the hair later 
            this.angleHairs=0;

            // Body of the pilot
            var bodyGeom = new THREE.BoxGeometry(15,15,15);
            var bodyMat = new THREE.MeshPhongMaterial({color:Colors.brown, shading:THREE.FlatShading});
            var body = new THREE.Mesh(bodyGeom, bodyMat);
            body.position.set(2,-12,0);
            this.mesh.add(body);

            // Face of the pilot
            var faceGeom = new THREE.BoxGeometry(10,10,10);
            var faceMat = new THREE.MeshLambertMaterial({color:Colors.pink});
            var face = new THREE.Mesh(faceGeom, faceMat);
            this.mesh.add(face);

            // Hair element
            var hairGeom = new THREE.BoxGeometry(4,4,4);
            var hairMat = new THREE.MeshLambertMaterial({color:Colors.brown});
            var hair = new THREE.Mesh(hairGeom, hairMat);
            // Align the shape of the hair to its bottom boundary, that will make it easier to scale.
            hair.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,2,0));
            
            // create a container for the hair
            var hairs = new THREE.Object3D();

            // create a container for the hairs at the top 
            // of the head (the ones that will be animated)
            this.hairsTop = new THREE.Object3D();

            // create the hairs at the top of the head 
            // and position them on a 3 x 4 grid
            for (var i=0; i<12; i++){
                var h = hair.clone();
                var col = i%3;
                var row = Math.floor(i/3);
                var startPosZ = -4;
                var startPosX = -4;
                h.position.set(startPosX + row*4, 0, startPosZ + col*4);
                this.hairsTop.add(h);
            }
            hairs.add(this.hairsTop);

            // create the hairs at the side of the face
            var hairSideGeom = new THREE.BoxGeometry(12,4,2);
            hairSideGeom.applyMatrix(new THREE.Matrix4().makeTranslation(-6,0,0));
            var hairSideR = new THREE.Mesh(hairSideGeom, hairMat);
            var hairSideL = hairSideR.clone();
            hairSideR.position.set(8,-2,6);
            hairSideL.position.set(8,-2,-6);
            hairs.add(hairSideR);
            hairs.add(hairSideL);

            // create the hairs at the back of the head
            var hairBackGeom = new THREE.BoxGeometry(2,8,10);
            var hairBack = new THREE.Mesh(hairBackGeom, hairMat);
            hairBack.position.set(-1,-4,0)
            hairs.add(hairBack);
            hairs.position.set(-5,5,0);

            this.mesh.add(hairs);

            var glassGeom = new THREE.BoxGeometry(5,5,5);
            var glassMat = new THREE.MeshLambertMaterial({color:Colors.brown});
            var glassR = new THREE.Mesh(glassGeom,glassMat);
            glassR.position.set(6,0,3);
            var glassL = glassR.clone();
            glassL.position.z = -glassR.position.z

            var glassAGeom = new THREE.BoxGeometry(11,1,11);
            var glassA = new THREE.Mesh(glassAGeom, glassMat);
            this.mesh.add(glassR);
            this.mesh.add(glassL);
            this.mesh.add(glassA);

            var earGeom = new THREE.BoxGeometry(2,3,2);
            var earL = new THREE.Mesh(earGeom,faceMat);
            earL.position.set(0,0,-6);
            var earR = earL.clone();
            earR.position.set(0,0,6);
            this.mesh.add(earL);
            this.mesh.add(earR);
        }
        Pilot.prototype.updateHairs = function(){
            // get the hair
            var hairs = this.hairsTop.children;
            // update them according to the angle angleHairs
            var l = hairs.length;
            for (var i=0; i<l; i++){
                var h = hairs[i];
                // each hair element will scale on cyclical basis between 75% and 100% of its original size
                h.scale.y = .75 + Math.cos(this.angleHairs+i/3)*.25;
            }
            // increment the angle for the next frame
            this.angleHairs += 0.16;
        }
    </script>
</body>
</html>